transfer_manager で GCS へのファイルアップロードが高速化できるか確認してみた。
こんにちは、みかみです。
ミドリフグが飼いたくて探していたのですが、なかなか見つからないので、琉球メダカを飼い始めました。
正式名称は知りませんが、その辺の水辺で普通に獲れるメダカです。オス(多分)はグッピーに近い綺麗な体色で、なかなかかわいいです。
はじめに
1ヶ月以上前のことですが、Google Cloud ブログで気になる記事を見ました。
Google Cloud 環境でバッチ処理を実装する場合、GCS からのファイルのダウンロードやアップロードはよく使う処理ではないかと思います。
特にファイル数が多かったり1ファイルのサイズが大きいケースでは、ファイルのダウンロードやアップロードに時間がかかり、しばしば悩ましい問題に発展することがあります。
ということで。
やりたいこと
- Google Cloud Storage の Python ライブラリ
transfer_manager
を使うと、本当に GCS へのファイルアップロード処理が速くなるのか確認してみたい。
前提
google-cloud-storage
ライブラリはインストール済みであるものとします。 本エントリでは、Cloud Shell を使用しました。
また、Cloud Storage API の有効化と操作に必要な権限は付与済みです。
テスト用ファイルを準備
アップロード処理の確認に使用する、テスト用ファイルを準備します。
以下のスクリプトを実行して、10MB のファイルを100個作成しました。
#!/bin/bash
FILE_SIZE=10000000
FILE_COUNT=100
FILE_PREFIX="sample_10MB_"
TARGET_DIR="files"
# フォルダが存在しない場合は作成
if [ ! -d "$TARGET_DIR" ]; then
mkdir -p "$TARGET_DIR"
fi
# ファイル作成
for i in $(seq 1 $FILE_COUNT); do
FILE_NAME="${FILE_PREFIX}${i}"
dd if=/dev/urandom bs=$FILE_SIZE count=1 of="$TARGET_DIR/$FILE_NAME" status=none
echo "create $TARGET_DIR/$FILE_NAME"
done
echo "done."
$ bash ./create_sample_files.sh
create files/sample_10MB_1
create files/sample_10MB_2
create files/sample_10MB_3
(省略)
create files/sample_10MB_100
done.
$ ls -la
total 16
drwxrwxr-x 3 mikami_yuki mikami_yuki 4096 Sep 24 15:20 .
drwxr-xr-x 67 mikami_yuki 1001 4096 Sep 24 15:20 ..
-rw-rw-r-- 1 mikami_yuki mikami_yuki 457 Sep 24 15:20 create_sample_files.sh
drwxrwxr-x 2 mikami_yuki mikami_yuki 4096 Sep 24 15:20 files
$ ls -la files/
total 976820
drwxrwxr-x 2 mikami_yuki mikami_yuki 4096 Sep 24 15:20 .
drwxrwxr-x 3 mikami_yuki mikami_yuki 4096 Sep 24 15:20 ..
-rw-rw-r-- 1 mikami_yuki mikami_yuki 10000000 Sep 24 15:20 sample_10MB_1
-rw-rw-r-- 1 mikami_yuki mikami_yuki 10000000 Sep 24 15:20 sample_10MB_10
-rw-rw-r-- 1 mikami_yuki mikami_yuki 10000000 Sep 24 15:20 sample_10MB_100
-rw-rw-r-- 1 mikami_yuki mikami_yuki 10000000 Sep 24 15:20 sample_10MB_11
(省略)
-rw-rw-r-- 1 mikami_yuki mikami_yuki 10000000 Sep 24 15:20 sample_10MB_99
直列処理でテスト用ファイルをアップロード
まずは、google.cloud.storage
ライブラリの upload_from_filename
メソッドで、直列処理で GCS にアップロードしてみます。
特にパフォーマンスの考慮はしておらず、アップロード前後の時間から、処理時間を計測して print
します。
以下の Python コードを実行しました。
from google.cloud import storage
import time
import os
def upload_files(bucket_name, filenames, destination_path, directory):
storage_client = storage.Client()
bucket = storage_client.bucket(bucket_name)
start_time = time.time() # アップロード開始時刻
for filename in filenames:
blob_name = os.path.join(destination_path, filename)
blob = bucket.blob(blob_name)
blob.upload_from_filename(os.path.join(directory, filename))
end_time = time.time() # アップロード終了時刻
elapsed_time = end_time - start_time # 経過時間
print(f"アップロード時間: {elapsed_time:.2f} 秒")
bucket_name = "test-mikami"
directory = "./files"
destination_path = "upload_files/serial"
# ディレクトリ内のファイル名を取得
filenames = [f for f in os.listdir(directory) if os.path.isfile(os.path.join(directory, f))]
# 直列アップロード
upload_files(bucket_name, filenames, destination_path, directory)
実行結果は以下です。
$ python serial.py
アップロード時間: 69.10 秒
結構かかりましたね。。
1分10秒ほどかかりました。
transfer_manager を使ってアップロード
同様に、google.cloud.storage.transfer_manager
の upload_many_from_filenames
でアップロードする場合の処理時間を確認します。
以下のコードを実行しました。
from google.cloud import storage
from google.cloud.storage import transfer_manager
import time
import os
def upload_files(bucket_name, filenames, source_directory="", blob_name_prefix="", workers=8):
storage_client = storage.Client()
bucket = storage_client.bucket(bucket_name)
start_time = time.time() # アップロード開始時刻
results = transfer_manager.upload_many_from_filenames(
bucket, filenames, source_directory=source_directory, blob_name_prefix=blob_name_prefix, max_workers=workers
)
end_time = time.time() # アップロード終了時刻
elapsed_time = end_time - start_time # 経過時間
print(f"アップロード時間: {elapsed_time:.2f} 秒")
bucket_name = "test-mikami"
directory = "./files"
destination_path = "upload_files/transfer_manager/"
# ディレクトリ内のファイル名を取得
filenames = [f for f in os.listdir(directory) if os.path.isfile(os.path.join(directory, f))]
# Transfer Manager でアップロード
upload_files(bucket_name, filenames, source_directory=directory, blob_name_prefix=destination_path)
結果は以下です。
$ python transfer_manager.py
アップロード時間: 10.06 秒
先ほど1分以上かかったアップロード処理が、約10秒で完了しました。
並列処理でアップロード
では、transfer_manager
を使わずに、ThreadPoolExecutor
を使って並列でアップロードしてみたらどうでしょうか?
transfer_manager
での実行と同様、max_workers=8
を指定して、並列でアップロード処理を実行します。
以下のコードを実行しました。
from google.cloud import storage
import time
import os
from concurrent.futures import ThreadPoolExecutor
def upload_files(bucket_name, filenames, destination_path, directory):
storage_client = storage.Client()
bucket = storage_client.bucket(bucket_name)
start_time = time.time() # アップロード開始時刻
with ThreadPoolExecutor(max_workers=8) as executor:
for filename in filenames:
blob_name = os.path.join(destination_path, filename)
blob = bucket.blob(blob_name)
executor.submit(blob.upload_from_filename, os.path.join(directory, filename))
end_time = time.time() # アップロード終了
elapsed_time = end_time - start_time # 経過時間
print(f"アップロード時間: {elapsed_time:.2f} 秒")
bucket_name = "test-mikami"
directory = "./files"
destination_path = "upload_files/parallel"
# ディレクトリ内のファイル名を取得
filenames = [f for f in os.listdir(directory) if os.path.isfile(os.path.join(directory, f))]
# 並列アップロード
upload_files(bucket_name, filenames, destination_path, directory)
結果は以下です。
$ python parallel.py
アップロード時間: 9.91 秒
transfer_manager
を使った時とほぼ同じ、10秒ほどでアップロードできました。
サイズの大きいファイルをアップロード
先ほどは 10MB × 100個 のファイルをアップロードしましたが、ファイルサイズが大きい場合はどうなるのでしょうか?
テスト用ファイル作成スクリプトの FILE_SIZE
と FILE_COUNT
の値を変更して、100MB × 10個 のテスト用ファイルを作成し、同様の Python コードでアップロードを実行してみました。
結果は以下です。
- 直列アップロード
$ python serial.py
アップロード時間: 17.23 秒
- transfer_manager でアップロード
$ python transfer_manager.py
アップロード時間: 4.57 秒
- 並列アップロード
$ python parallel.py
アップロード時間: 4.24 秒
直列でもそれほど時間がかからないこともありますが、速度向上率がそれほど高くないことから、ファイルをチャンクに分けてアップロードするような処理は入っていないように思われます。
transfer_manager のコードを確認
transfer_manager
の upload_many_from_filenames
の実装を確認してみます。
upload_many_from_filenames
からは upload_many
メソッドを呼び出しており、
upload_many
では _get_pool_class_and_requirements
を使って executor を作成しています。
executor の実態を確認してみると
concurrent.futures.ProcessPoolExecutor
クラスを返却していました。
ファイルアップロード処理など、CPU使用率がそれほど高くない I/O バウンドなタスクは、ProcessPoolExecutor
よりも ThreadPoolExecutor
を使った方が効率的に処理できるのではないかと思いましたが、いずれにせよ、自分で並列処理の実装が不要なのは嬉しい限りです。
まとめ(所感)
transfer_manager
でファイルアップロード処理速度が向上することが確認できました。
今回は大量ファイル向けの upload_many
の処理速度を確認してみましたが、transfer_manager
にはサイズの大きいファイルをチャンクに分けて効率的にアップロードするインターフェース(upload_chunks_concurrently
)もあるようです。
transfer_manager
を使えば、並列処理やファイルのチャンク化などを自分で考慮する必要なく、効率的で保守性の高いコードをシンプルに実装することができます。
今後は積極的に transfer_manager
を利用しようと思いました!
参考
- クライアント ライブラリを使用してアップロードとダウンロードを高速化| Google Cloud ブログ
- 多数のオブジェクトをアップロードする | Cloud Storage ドキュメント
- XML API マルチパート アップロード | Cloud Storage ドキュメント
- upload_many_from_filenames | google-cloud-storage Reference
- python-storage/google/cloud/storage/transfer_manager.py | GitHub
- python-storage/samples/snippets/storage_transfer_manager.py | GitHub